El concepto de Execution Context es sumamente vital para entender como funciona Javascript. Hay 2 tipos:
- Global Execution Context (GEC)
- Function Execution Context (FEC)
Y cada uno se compone de 2 fases:
- Fase De Creación – Se preparan los objeto, los espacios en memoria y las referencias
- Fase de Ejecución – Se asignan los valores de las variables
El que veremos en este artículo será el FEC. Así que si ya sabes como funciona el Global Execution Context estas en el lugar indicado.
De lo contrario hechale un vistazo al único ejemplo que necesitas para entender GEC. Es necesario que entiendas como funciona antes de continuar.
Dicho esto empecemos.
Repaso: ¿Cómo se conforma?
*Este extracto es el mismo que se encuentra para el ejemplo de Global Execution Context. Puedes saltartelo con confianza si ya lo conoces.
Execution Context
Los primeros objetos que se encuentran dentro de un Execution Context son los Lexical Environments.
Un lexical environment (ambiente léxico) es una estructura que contiene las referencias a los identificadores distintas propiedades. Es decir referencias a variables y funciones declaradas.
Solo hay 2 tipos de Lexical Environment y todos los Execution Contexts los tienen (ya sea los utilizen o no):
- Lexical Environment – Es el ambiente primario y guarda toda las funciones y todas las variables declaradas con
let
yconst
. - Variable Environment – Es el ambiente utilizado para variables declaradas con
var
.
Lexical Environment/Variable Environment
A su vez cada Lexical Environment contiene 2 objetos indispensables:
- Environment Record – Es donde se almacenan las referencias de las variables y funciones. Este se encuentra compuesto de otros Environment Records.
Es importante que tengas en cuenta que solo las variables declaradas convar
se alamacenan aquí durante esta fase. Variables decalradas conlet
yconst
se guardan en un lugar comúnmente referido por la comunidad como TDZ (Temporal Data Zone o Temporal Dead Zone). - OuterReference – Es la referencia al Lexical Environment anterior (también llamado Lexical Environment padre).
*Muchas veces encontrarás que se menciona que el Lexical Environment contiene tambíen a la referencia this
. Esto es técnicamente correcto, pero siendo más específicos esta referencia se encuentra dentro de un los Environment Records internos.
Environment Record
Por último cada Environment Record puede ser 1 de 3 tipos diferentes y cada uno contiene diferentes elementos:
El Declarative Environment Record guarda referencias a conceptos que se explican por sí mismos. Esto es lo que permite que se manden a llamar funciones y variables directamente.
Los 3 elementos que componen al Object Environment Record se refieren a:
- Variable Object (VO) – Es el objeto que almacena las variables declaradas con var y funciones declaradas. Este objeto es el mismo que el objeto global, por lo que solo esta presente dentro de un Global Execution Context. Las variables declradas con
let
yconst
se mantienen fuera del scope de este objeto por lo que solo se pueden llamar gracias al Declarative Environment Record. - Argument Object (AO) – Es el objeto especial arguments que contiene los parámetros que se pasaron a una función. Por esto mismo solo esta presente dentro de un Function Execution Context.
- this Binding– La keyword
this
referencia al objeto que creo el Execution Context.
Finalmente el Global Environment Record solo funciona como un objeto compuesto de los otros 2 Environment Records. A pesar de ser llamado global por lógica dentro de un FEC también puede existir un Environment Record que este compuesto de los otros 2.
Function Execution Context – El Ejemplo
Los conceptos que se usan para entender FEC son los mismos que con GEC. Si necesitas repasarlos puedes hacer uso de la sección anterior. Ahora…observa y razona como esta estructurado el siguiente bloque de código:
let a = 10;
const b = 20;
var c = 30;
var callerObject = {
calledFunction: multiply
}
function multiply(d, e) {
let f = 40;
const g = 50;
var h= 60;
function inner() {
console.log('I am an inner function');
}
return d*e
}
callerObject.calledFunction(a,b)
Con este bloque se crearán 2 Execution Context: un GEC para todo el script y un FEC para la función multiply.
Global Execution Context – La Base
A pesar de ser un ejemplo de Function Execution Context tienes que tener claro que esta haciendo el GEC durante este período.
Primero Javascript crea el Global Execution Context. Como ya debes de saber, este proceso pasa por 2 fases: creación y ejecución.
Por el momento nos saltaremos la fase de creación del mismo. A estas alturas ya la debes de tener clara.
Una vez se inicia la fase de ejecución ocurre algo interesante:
JS agrega el GEC al Execution Stack.
¿Qué es eso del Execution Stack?
El Execution Stack o Call Stack no es mas que una pila de contextos de ejecución que JS usa para dar seguimiento al orden que debe seguir al ejecutar instrucciones.
Posteriormente continua la fase de ejecución como es normal, línea por línea va resolviendo los valores de las variables. Al final de ese proceso y justo antes de llamar a callerObject.calledFunction()
el estatus del GEC es el siguiente:
–Ok… ¿y en qué momento se crea el Function Execution Context? — te preguntarás.
Un FEC se crea cada vez que se ejecuta una función. En este caso se empezará su proceso de creación en el momento en el que se ejecute la última línea de nuestro código donde se manda a llamar la función de multiply
a través del objeto callerObject
.
callerObject.calledFunction(a,b) ---> multiply(a,b)
¡Ahora si! Comienza el proceso de creación del Function Execution Context de la función multiply
.
Fase de Creación
Al igual que con el GEC Javascript crea 2 lexical environments para el FEC.
1. El objeto inicial
2. Posteriormente JS define el AO, dentro del Object Environment Record de ambos Lexical Environments. El Arguments Object contiene los parámetros que se hayan pasado a la función. Estos parámetros también se colocan en el Declarative Environment Record. Como si hubieran sido declarados con var
.
El Arguments Object se comporta como un arreglo así que puedes acceder a los parámetros de la misma manera así como revisar el tamaño del mismo.
3. Javascript encuentra 1 variable declarada con var y al hacer el proceso de hoisting la guarda en el Declarative Environment Record con valor de undefined
.
4. JS lee las variables declaradas con let
y const
y las funciones. La función inner
se guarda en el Declarative Environment Record. Las variables f
y g
y se agregan al TDZ.
5. El siguiente paso para Javascript es asignar el valor de la Outer Reference. Este valor hace referencia al Execution Context anterior (o padre).
*La asignación del Outer Reference se hace en tanto el Lexical como el Variable Environment.
6. El último paso es asignar el valor de this
. Cómo recordaras el valor que se le asigna es el del objeto desde el cual se llamó.
En este caso multiply
fue llamado por la propiedad calledFunction
del objeto callerObject
. Por esto mismo this
toma como su valor de referencia al objeto callerObject
.
*La asignación de this
también se hace en tanto el Lexical como el Variable Environment.
Fase de Ejecución
En la fase de ejecución, el engine de Javascript lee el código una vez mas y conforme va encontrando líneas donde se asignan valores va actualizando las referencias correspondientes en los lexical environments.
Observa la función de nuevo:
let a = 10;
const b = 20;
var c = 30;
var callerObject = {
calledFunction: multiply
}
function multiply(d, e) {
let f = 40;
const g = 50;
var h= 60;
function inner() {
console.log('I am an inner function');
}
return d*e
}
callerObject.calledFunction(a,b)
El primer paso que hace JS es agregar este nuevo Execution Context al Execution Stack:
*El GEC no ha terminado de ejecutarse. JS pondrá en pausa su ejecución hasta que termine de ejecutar los contexts que se encuentren más arriba en el stack.
Posteriormente JS empieza a ejecutar línea por línea a multiply
:
Línea 7 (La primera de la función multiply
): Asigna los valores de los argumentos d
y e
.
En este caso son 10 y 20 respectivamente.
Línea 8 : Asigna 40 a la variable f
.
Se saca la variable de TDZ y se coloca su valor dentro del Lexical Environment.
Línea 9: Asigna 50 a la variable g
.
Se saca la variable de TDZ y se coloca su valor dentro del Lexical Environment.
Línea 10: Se asigna el valor de 60 a h
. Este valor no estaba en TDZ, estaba en el Variable Environment, así que solo se actualiza.
Línea 12-14: Contiene la definición de la función inner
pero ya se habia guardado la referencia a la misma durante el proceso de hoisting en la fase de creación.
Línea 16: Se regresa el resultado de la multiplicación de los valores
En este momento se termina la ejecución de la función multiply
por lo que se también se termina su FEC correspondiente y se quita del Execution Stack.
Esto deja solo al GEC en el Execution Stack.
Con este termina la fase de ejecución de el FEC de multiply
.
Más alla de la vida del FEC
En el momento que termina la ejecución de multiply
JS retoma la ejecución del GEC que se había pausado hasta este momento. Sin embargo en este ejemplo JS se encuentra con el final del archivo. No hay nada mas que ejecutar. Por lo tanto se termina con el GEC y se saca del Execution Stack.
Esto deja el Execution Stack vacío lo que nos lleva a la finalización de la ejecución del código.
Con esto ya debes de tener claro como funciona el FEC (y si me hiciste caso al principio, el GEC también). ¿De qué sirve este como lo aplicas?
La próxima vez que estes creando o actualizando algo en código Javascript intenta imaginarte que está haciendo el motor del lenguaje mientras compilas. Eventual e inevtiablemente, te toparás con un bug. Al intentar resolverlo te darás cuenta que te es más fácil imaginar el camino que sigue la variable y podrás averiguar que estás sucediendo de manera más ágil.
Te aseguro que este nuevo conocimiento te servirá para corregir mucho más rápido al menos el 50% de los errores con los que te encuentres.